/*
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "odin_types.h"
#include "odin_util.h"
#include "odin_globals.h"
#include "netlist_utils.h"
#include "hard_blocks.h"
#include "memories.h"

STRING_CACHE* hard_block_names = NULL;

void cache_hard_block_names();
void register_hb_port_size(t_model_ports* hb_ports, int size);

void register_hb_port_size(t_model_ports* hb_ports, int size) {
    if (hb_ports)
        hb_ports->size = size;
    /***
     * else
     *	TODO error
     */
}

t_model_ports* get_model_port(t_model_ports* ports, const char* name) {
    while (ports && strcmp(ports->name, name))
        ports = ports->next;

    return ports;
}

void cache_hard_block_names() {
    t_model* hard_blocks = NULL;

    hard_blocks = Arch.models;
    hard_block_names = sc_new_string_cache();
    while (hard_blocks) {
        sc_add_string(hard_block_names, hard_blocks->name);
        hard_blocks = hard_blocks->next;
    }
}

void register_hard_blocks() {
    cache_hard_block_names();
    single_port_rams = find_hard_block(SINGLE_PORT_RAM_string);
    dual_port_rams = find_hard_block(DUAL_PORT_RAM_string);

    if (single_port_rams) {
        if (configuration.split_memory_width) {
            register_hb_port_size(
                get_model_port(single_port_rams->inputs, "data"), 1);

            register_hb_port_size(
                get_model_port(single_port_rams->outputs, "out"), 1);
        }

        register_hb_port_size(
            get_model_port(single_port_rams->inputs, "addr"), get_sp_ram_split_depth());
    }

    if (dual_port_rams) {
        if (configuration.split_memory_width) {
            register_hb_port_size(
                get_model_port(dual_port_rams->inputs, "data1"), 1);
            register_hb_port_size(
                get_model_port(dual_port_rams->inputs, "data2"), 1);

            register_hb_port_size(
                get_model_port(dual_port_rams->outputs, "out1"), 1);
            register_hb_port_size(
                get_model_port(dual_port_rams->outputs, "out2"), 1);
        }

        int split_depth = get_dp_ram_split_depth();

        register_hb_port_size(
            get_model_port(dual_port_rams->inputs, "addr1"), split_depth);
        register_hb_port_size(
            get_model_port(dual_port_rams->inputs, "addr2"), split_depth);
    }
}

void deregister_hard_blocks() {
    sc_free_string_cache(hard_block_names);
    return;
}

t_model* find_hard_block(const char* name) {
    t_model* hard_blocks;

    hard_blocks = Arch.models;
    while (hard_blocks)
        if (!strcmp(hard_blocks->name, name))
            return hard_blocks;
        else
            hard_blocks = hard_blocks->next;

    return NULL;
}

void define_hard_block(nnode_t* node, FILE* out) {
    int i, j;
    int index, port;
    int count;
    char buffer[MAX_BUF];

    /* Assert that every hard block has at least an input and output */
    oassert(node->input_port_sizes[0] > 0);
    oassert(node->output_port_sizes[0] > 0);

    //IF the hard_blocks is an adder or a multiplier, we ignore it.(Already print out in define_add_function and define_mult_function)
    if (strcmp(node->related_ast_node->identifier_node->types.identifier, "multiply") == 0 || strcmp(node->related_ast_node->identifier_node->types.identifier, "adder") == 0)
        return;

    count = fprintf(out, "\n.subckt ");
    count--;
    count += fprintf(out, "%s", node->related_ast_node->identifier_node->types.identifier);

    /* print the input port mappings */
    port = index = 0;
    for (i = 0; i < node->num_input_pins; i++) {
        /* Check that the input pin is driven */
        if (node->input_pins[i]->net->num_driver_pins == 0
            && node->input_pins[i]->net != verilog_netlist->zero_net
            && node->input_pins[i]->net != verilog_netlist->one_net
            && node->input_pins[i]->net != verilog_netlist->pad_net) {
            warning_message(NETLIST, node->loc, "Signal %s is not driven. padding with ground\n", node->input_pins[i]->name);
            add_fanout_pin_to_net(verilog_netlist->zero_net, node->input_pins[i]);
        } else if (node->input_pins[i]->net->num_driver_pins > 1) {
            error_message(NETLIST, node->loc, "Multiple (%d) driver pins not supported in hard block definition\n", node->input_pins[i]->net->num_driver_pins);
        }

        if (node->input_port_sizes[port] == 1) {
            if (node->input_pins[i]->net->driver_pins[0]->name != NULL)
                j = odin_sprintf(buffer, " %s=%s", node->input_pins[i]->mapping, node->input_pins[i]->net->driver_pins[0]->name);
            else
                j = odin_sprintf(buffer, " %s=%s", node->input_pins[i]->mapping, node->input_pins[i]->net->driver_pins[0]->node->name);
        } else {
            if (node->input_pins[i]->net->driver_pins[0]->name != NULL)
                j = odin_sprintf(buffer, " %s[%d]=%s", node->input_pins[i]->mapping, index, node->input_pins[i]->net->driver_pins[0]->name);
            else
                j = odin_sprintf(buffer, " %s[%d]=%s", node->input_pins[i]->mapping, index, node->input_pins[i]->net->driver_pins[0]->node->name);
        }

        if (count + j > 79) {
            fprintf(out, "\\\n");
            count = 0;
        }
        count += fprintf(out, "%s", buffer);

        index++;
        if (node->input_port_sizes[port] == index) {
            index = 0;
            port++;
        }
    }

    /* print the output port mappings */
    port = index = 0;
    for (i = 0; i < node->num_output_pins; i++) {
        if (node->output_port_sizes[port] != 1)
            j = odin_sprintf(buffer, " %s[%d]=%s", node->output_pins[i]->mapping, index, node->output_pins[i]->name);
        else
            j = odin_sprintf(buffer, " %s=%s", node->output_pins[i]->mapping, node->output_pins[i]->name);

        if (count + j > 79) {
            fprintf(out, "\\\n");
            count = 0;
        }
        count += fprintf(out, "%s", buffer);

        index++;
        if (node->output_port_sizes[port] == index) {
            index = 0;
            port++;
        }
    }

    count += fprintf(out, "\n\n");
    return;
}

void output_hard_blocks(FILE* out) {
    t_model_ports* hb_ports;
    t_model* hard_blocks;
    char buffer[MAX_BUF];
    int count;
    int i;

    oassert(out != NULL);
    hard_blocks = Arch.models;
    while (hard_blocks != NULL) {
        if (hard_blocks->used == 1) /* Hard Block is utilized */
        {
            //IF the hard_blocks is an adder or a multiplier, we ignore it.(Already print out in add_the_blackbox_for_adds and add_the_blackbox_for_mults)
            if (strcmp(hard_blocks->name, "adder") == 0 || strcmp(hard_blocks->name, "multiply") == 0) {
                hard_blocks = hard_blocks->next;
                break;
            }

            fprintf(out, "\n.model %s\n", hard_blocks->name);
            count = fprintf(out, ".inputs");
            hb_ports = hard_blocks->inputs;
            while (hb_ports != NULL) {
                for (i = 0; i < hb_ports->size; i++) {
                    if (hb_ports->size == 1)
                        count = count + odin_sprintf(buffer, " %s", hb_ports->name);
                    else
                        count = count + odin_sprintf(buffer, " %s[%d]", hb_ports->name, i);

                    if (count >= 78)
                        count = fprintf(out, " \\\n%s", buffer) - 3;
                    else
                        fprintf(out, "%s", buffer);
                }
                hb_ports = hb_ports->next;
            }

            count = fprintf(out, "\n.outputs") - 1;
            hb_ports = hard_blocks->outputs;
            while (hb_ports != NULL) {
                for (i = 0; i < hb_ports->size; i++) {
                    if (hb_ports->size == 1)
                        count = count + odin_sprintf(buffer, " %s", hb_ports->name);
                    else
                        count = count + odin_sprintf(buffer, " %s[%d]", hb_ports->name, i);

                    if (count >= 78)
                        count = fprintf(out, " \\\n%s", buffer) - 3;
                    else
                        fprintf(out, "%s", buffer);
                }
                hb_ports = hb_ports->next;
            }

            fprintf(out, "\n.blackbox\n.end\n\n");
        }
        hard_blocks = hard_blocks->next;
    }

    return;
}

void instantiate_hard_block(nnode_t* node, short mark, netlist_t* /*netlist*/) {
    int i, port, index;

    port = index = 0;
    /* Give names to the output pins */
    for (i = 0; i < node->num_output_pins; i++) {
        if (node->output_pins[i]->name == NULL)
            node->output_pins[i]->name = make_full_ref_name(node->name, NULL, NULL, node->output_pins[i]->mapping, -1);

        index++;
        if (node->output_port_sizes[port] == index) {
            index = 0;
            port++;
        }
    }

    node->traverse_visited = mark;
    return;
}

int hard_block_port_size(t_model* hb, char* pname) {
    t_model_ports* tmp;

    if (hb == NULL)
        return 0;

    /* Indicates that the port size is different for this hard block
     *  depending on the instance of the hard block. May want to extend
     *  this list of blocks in the future.
     */
    if ((strcmp(hb->name, SINGLE_PORT_RAM_string) == 0) || (strcmp(hb->name, DUAL_PORT_RAM_string) == 0)) {
        return -1;
    }

    tmp = hb->inputs;
    while (tmp != NULL)
        if ((tmp->name != NULL) && (strcmp(tmp->name, pname) == 0))
            return tmp->size;
        else
            tmp = tmp->next;

    tmp = hb->outputs;
    while (tmp != NULL)
        if ((tmp->name != NULL) && (strcmp(tmp->name, pname) == 0))
            return tmp->size;
        else
            tmp = tmp->next;

    return 0;
}

enum PORTS
hard_block_port_direction(t_model* hb, char* pname) {
    t_model_ports* tmp;

    if (hb == NULL)
        return ERR_PORT;

    tmp = hb->inputs;
    while (tmp != NULL)
        if ((tmp->name != NULL) && (strcmp(tmp->name, pname) == 0))
            return tmp->dir;
        else
            tmp = tmp->next;

    tmp = hb->outputs;
    while (tmp != NULL)
        if ((tmp->name != NULL) && (strcmp(tmp->name, pname) == 0))
            return tmp->dir;
        else
            tmp = tmp->next;

    return ERR_PORT;
}
